﻿using System;
using Beaker.Common.Specs.Builders.Instance;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Beaker.Common.Specs
{
	[TestClass]
	public class MaybeSpecs
	// cannot use specs because that in turn uses the thing we trying to test
	{
		#region HasValue

		[TestMethod]
		public void HasValueIsTrueForMaybeWithAValue()
		{
			var subject = new MaybeBuilder<string>()
				.WrappingValue("Foo")
				.Build();

			subject.HasValue
				.Should().BeTrue();
		}

		[TestMethod]
		public void HasValueIsTrueForMaybeWithANullValue()
		{
			var subject = new MaybeBuilder<string>()
				.WrappingValue(null)
				.Build();

			subject.HasValue
				.Should().BeTrue();
		}

		[TestMethod]
		public void HasValueIsFalseForMaybeWithoutAValue()
		{
			var subject = new MaybeBuilder<string>()
				.Build();

			subject.HasValue
				.Should().BeFalse();
		}

		#endregion
		#region IMaybe.GetUnderlyingType

		[TestMethod]
		public void MaybeCastToBaseInterfaceReturnsCorrectTypeViaUnderlyingType()
		{
			IMaybe subject = new MaybeBuilder<string>()
				.Build();

			subject.GetUnderlyingType()
				.Should().Be(typeof(string));
		}

		#endregion
		#region GetValueOrDefault

		[TestMethod]
		public void WhenCallingGetValueOrDefaultAndMaybeHasValueTheValueIsReturned()
		{
			var subject = new MaybeBuilder<int>()
				.WrappingValue(25)
				.Build();

			var result = subject.GetValueOrDefault(30);

			result.Should().Be(25, because: "the maybe has a value and thus that value is returned, not the default passed");
		}

		[TestMethod]
		public void WhenCallingGetValueOrDefaultAndMaybeHasNoValueTheProvidedDefaultValueIsReturned()
		{
			var subject = new MaybeBuilder<int>()
				.Build();

			var result = subject.GetValueOrDefault(30);

			result.Should().Be(30, because: "the maybe has no value and thus the default should be returned");
		}

		[TestMethod]
		public void WhenCallingGetValueOrDefaultWithProviderAndMaybeHasValueTheValueIsReturned()
		{
			var subject = new MaybeBuilder<int>()
				.WrappingValue(25)
				.Build();

			bool isCalled = false;
			var result = subject.GetValueOrDefault(() => { isCalled = true; return 30; });

			isCalled.Should().BeFalse(because: "the provided should never be called");
			result.Should().Be(25, because: "the maybe has a value and thus that value is returned, not the default passed");
		}

		[TestMethod]
		public void WhenCallingGetValueOrDefaultWithProviderAndMaybeHasNoValueTheProviderIsCalledAndItsValueIsReturned()
		{
			var subject = new MaybeBuilder<int>()
				.Build();

			bool isCalled = false;
			var result = subject.GetValueOrDefault(() => { isCalled = true; return 30; });

			isCalled.Should().BeTrue(because: "the provided should be called");
			result.Should().Be(30, because: "the maybe has no value and thus the default should be returned");
		}
		
		#endregion
		#region Comparsion

		[TestMethod]
		public void TwoMaybesWithNoValueAreEqual()
		{
			var subject1 = new MaybeBuilder<int>()
				.Build();
			var subject2 = new MaybeBuilder<int>()
				.Build();

			var result = subject1.Equals(subject2);

			result.Should().BeTrue(because: "two maybes without a value should be equal");
		}

		[TestMethod]
		public void OneMaybeWithValueAndOneWithoutAreNotEqual()
		{
			var subject1 = new MaybeBuilder<int>()
				.Build();
			var subject2 = new MaybeBuilder<int>()
				.WrappingValue(25)
				.Build();

			var result = subject1.Equals(subject2);

			result.Should().BeFalse(because: "a maybe with a value is never equal to a maybe without a value");
		}

		[TestMethod]
		public void TwoMaybesWithSameValueAreEqual()
		{
			var subject1 = new MaybeBuilder<int>()
				.WrappingValue(25)
				.Build();
			var subject2 = new MaybeBuilder<int>()
				.WrappingValue(25)
				.Build();

			var result = subject1.Equals(subject2);

			result.Should().BeTrue(because: "two maybes with the same value are equal");
		}

		[TestMethod]
		public void TwoMaybesWithDifferentValueAreNotEqual()
		{
			var subject1 = new MaybeBuilder<int>()
				.WrappingValue(25)
				.Build();
			var subject2 = new MaybeBuilder<int>()
				.WrappingValue(15)
				.Build();

			var result = subject1.Equals(subject2);

			result.Should().BeFalse(because: "two maybes with a different value are not equal");
		}

		[TestMethod]
		public void MaybeWithoutValueIsNotEqualsToAValue()
		{
			var subject = new MaybeBuilder<int>()
				.Build();

			var result = subject.Equals(25);

			result.Should().BeFalse(because: "a maybe without a value is never equal to a value");
		}

		[TestMethod]
		public void MaybeWithDifferentValueIsNotEqualsToAnotherValue()
		{
			var subject = new MaybeBuilder<int>()
				.WrappingValue(15)
				.Build();

			var result = subject.Equals(25);

			result.Should().BeFalse(because: "a maybe with a value of 15 should not be equal to 25.");
		}

		[TestMethod]
		public void MaybeWithSameValueIsEqualsToComparedValue()
		{
			var subject = new MaybeBuilder<int>()
				.WrappingValue(15)
				.Build();

			var result = subject.Equals(15);

			result.Should().BeTrue(because: "a maybe with a value of 15 should be equal to 15.");
		}

		#endregion
		# region Conversion

		[TestMethod]
		public void CastFromUnderlyingTypeToMaybe()
		{
			Maybe<int> value = 25;
			value.Value.Should().Be(25, because: "implicit assign should correctly create maybe with same value");
		}

		[TestMethod]
		public void ExplicitCastFromMaybeToUnderlyingTypeShouldThrowWhenMaybeHasNoValue()
		{
			Maybe<int> subject = new Maybe<int>();

			int value;
			Action act = () => value = (int)subject;

			act.ShouldThrow<InvalidCastException>(because: "the maybe has no value so cannot be cast to underlying type");
		}

		[TestMethod]
		public void ExplicitCastFromMaybeToUnderlyingTypeShouldReturnActualValueWhenMaybeHasAValue()
		{
			Maybe<int> subject = 25;

			int value = (int)subject;

			value.Should().Be(25, because: "explict cast to underlying type should result in underlying value");
		}

		[TestMethod]
		public void CastFromTypesToObjectWorksImplicit()
		{
			Maybe<object> baseType = new Maybe<string>("FooBar");
			baseType.Value.Should().Be("FooBar");
		}

		[TestMethod]
		public void CastFromObjectToTypeWorksExplicit()
		{
			Maybe<int> baseType = (Maybe<int>)new Maybe<object>(254);
			baseType.Value.Should().Be(254);
		}

		[TestMethod]
		public void CastFromObjectWithNullToNotNullableTypeThrows()
		{
			Maybe<int> result;
			Action act = () => result = (Maybe<int>)new Maybe<object>(null);

			act.ShouldThrow<NullReferenceException>();
		}

		#endregion
	}
}
